﻿using System;
using System.Collections.Generic;
using System.Globalization;         // used for file name
using System.IO;                    // writing to file
using System.Text;
using System.Threading;



namespace ShimmerConnect
{
     // Class that describes a individual SHIMMER device
    public class ShimmerDevice
    {
        // variable - const
        // these variables are double because they will be used in arithmatics, so to reduce the overhead of additional typecasting...
        private readonly double timeTickOffset = Math.Pow((double)2, (double)15); // add this much per rollover (2 ^ 15 = 32768, for the 32 kHz clock)
        private readonly double oneHundredNanoSec = Math.Pow((double)10, (double)9) / 100; // one DateTime.tick = 100 ns

        // variables - serial connects
        private System.IO.Ports.SerialPort serialPortShimmer;
        private Thread readThread;
        private string bluetoothId;

        // variables - save to file
        private bool pSaveToFile; // should the app save the streamed data to file
        private string filePathStr; // filepath to save streamed data
        private StreamWriter pCsvFile; // streamwriter that connects to the file

        // variable - settings and constants
        private ShimmerProfile pProfile; // shimmer profile
        private ShimmerApplicationSettings pSettings; // application settings

        private DateTime cmdStartTimeStamp; // timestamp when stream command is given
        private DateTime streamTimeStamp; // timestamp when the first timestamp is recieved
        private int streamTimeTick; // timetick when the first timestamp is recieved
        private int previousTimeTick; // previous timetick, to note rollover instances
        private int previousActualTimeTick; // DEBUG
        private int streamRollOver;  // shimmer clock rolls over at 16-bit
        private readonly int streamRollOverInc = (int)Math.Pow((double)2, (double)16); // add this much per rollover (2 ^ 16 = 65536)
        private int entriesRead; // keep track of how many entries has been logged

        // variable - state and status
        private volatile bool stopReading = false;
        private bool isStreaming = false;            // is the SHIMMER actively streaming data right now?
        private bool streamingDataRecieved = false;

        private volatile bool sensorsChangePending = false;

        // methods
        public ShimmerDevice(System.ComponentModel.IContainer pcomponents, ShimmerProfile ppProfile, ShimmerApplicationSettings ppSettings, string pport, string pBtname)
        {
            // constructor

            pSettings = ppSettings;

            serialPortShimmer = new System.IO.Ports.SerialPort(pcomponents);
            serialPortShimmer.BaudRate = pSettings.BaudRate;
            serialPortShimmer.ReadTimeout = pSettings.ReadTimeout;
            serialPortShimmer.PortName = pport;

            bluetoothId = pBtname;

            ProfileUpdate(ppProfile); // likely just some generic profile

            pSaveToFile = pSettings.SaveToFile; 
        }

        public void ProfileUpdate(ShimmerProfile ppProfile)
        {
            // update profile

            pProfile = ppProfile;            
        }

        public override string ToString()
        {
            return ToStringPortName();
        }

        public string ToStringPortName()
        {
            return serialPortShimmer.PortName;
        }

        public bool ProfileIsFilled()
        {
            return pProfile.GetIsFilled();
        }

        public void RecordCurrentTimeStamp(DateTime pCurrentTimeStamp)
        {
            // Reset the timers to prepare for a stream

            cmdStartTimeStamp = pCurrentTimeStamp;
            streamTimeTick = -1;
            previousTimeTick = streamRollOverInc; 
            streamRollOver = 0;
            entriesRead = 0;
        }

        public void CsvStartStream(string pfilePathStrPrefix, DateTime pCurrentTimeStamp)
        { 
            // start the file writing stream if needed and applicable

            filePathStr = pfilePathStrPrefix + "_" + bluetoothId + "_" + serialPortShimmer.PortName + ".csv";
            RecordCurrentTimeStamp(pCurrentTimeStamp);

            if (pCsvFile == null && pSaveToFile)
            {
                pCsvFile = new StreamWriter(filePathStr, false);
                WriteHeaderToFile(); // save the header
            }
        }

        public void CsvStartStreamCommand()
        {
            // separated from CsvStartStream in an attempt to better sync start-stream times

            // System.Threading.Thread.Sleep(100);
            CommandStartStreaming();
        }

        public void CsvStopStream()
        {
            // stop if file writing stream

            CommandStopStreaming();
        }

        private void CsvNullStream()
        {
            if (pCsvFile != null)
            {
                pCsvFile.Flush();
                pCsvFile.Close();
                pCsvFile = null; // this is so we can tell if it's actually closed or not
            }
        }

        public void CommandStartStreaming()
        {
            // start data streaming
            if (SerialIsOpen() && (pProfile.GetIsFilled()))
            {
                serialPortShimmer.Write(new byte[1] { (byte)Shimmer.PacketType.StartStreamingCommand }, 0, 1);
                isStreaming = true;

                streamingDataRecieved = false;
            }
        }

        public void CommandStopStreaming()
        {
            if (SerialIsOpen())
            {
                serialPortShimmer.Write(new byte[1] { (byte)Shimmer.PacketType.StopStreamingCommand }, 0, 1);
                isStreaming = false;

           //     streamingDataRecieved = false;
            }
        }

        public void SerialOpen()
        {
            // open serial connection

            try
            {
                serialPortShimmer.Open();
                serialPortShimmer.DiscardInBuffer();
                serialPortShimmer.DiscardOutBuffer();

                ChangeConfiguration(); // update the profile, sync with local
            }
            catch (Exception)
            {
                throw;
            }
        }

        public void SerialClose()
        {
            // close serial connection
            stopReading = true;

            // the actual closing is done in ReadData, since the last bits of data might still need to be flushed
        }

        public void SerialClosePort()
        { 
            // actually closes the port

            serialPortShimmer.Close();
        }

        public bool SerialIsOpen()
        {
            // checks to see if the serial port is open

            return serialPortShimmer.IsOpen;
        }

        public void ReadDataStart()
        {
            // start the data streaming thread

            stopReading = false;
            readThread = new Thread(new ThreadStart(ReadData));
            readThread.Start();
        }

        public void ReadDataEnd()
        {
            // stop the data streaming

            readThread.Abort();
        }

        public void ProfileCheck()
        {
            if (SerialIsOpen())
            {
                serialPortShimmer.Write(new byte[1] { (byte)Shimmer.PacketType.InquiryCommand }, 0, 1);
            }
        }

        private void ReadData()
        {
            // read data thread 

            List<byte> buffer = new List<byte>();
            int i;
            serialPortShimmer.DiscardInBuffer();
            while (!stopReading)    //continues to read for approx 1s (until serial read timeout)
            {
                try
                {
                    switch (serialPortShimmer.ReadByte())
                    {
                        case (byte)Shimmer.PacketType.DataPacket:
                            if (pProfile.GetIsFilled())
                            {
                                for (i = 0; i < (pProfile.GetNumChannels() * 2) + 2; i++)
                                {
                                    buffer.Add((byte)serialPortShimmer.ReadByte());
                                }

                                ShimmerDataPacket packet = new ShimmerDataPacket(buffer, pProfile.GetNumChannels());

                                if (packet.GetIsFilled())
                                {
                                    SetTextChannels(packet);
                                    streamingDataRecieved = true; // data packets are starting to be recieved
                                }

                                buffer.Clear();
                            }
                            break;

                        case (byte)Shimmer.PacketType.InquiryResponse:
                            for (i = 0; i < 5; i++)
                            {
                                // get Sampling rate, accel sensitivity, config setup byte0, num chans and buffer size
                                buffer.Add((byte)serialPortShimmer.ReadByte());
                            }

                            for (i = 0; i < (int)buffer[3]; i++)
                            {
                                // read each channel type for the num channels
                                buffer.Add((byte)serialPortShimmer.ReadByte());
                            }

                            pProfile.fillProfile(buffer);
                            buffer.Clear();
                            break;

                        case (byte)Shimmer.PacketType.SamplingRateResponse:
                            pProfile.SetAdcSamplingRate(serialPortShimmer.ReadByte());
                            break;

                        case (byte)Shimmer.PacketType.AccelSensitivityResponse:
                            pProfile.SetAccelSensitivity(serialPortShimmer.ReadByte());
                            break;

                        case (byte)Shimmer.PacketType.ConfigSetupByte0Response:
                            pProfile.SetConfigSetupByte0(serialPortShimmer.ReadByte());
                            break;

                        case (byte)Shimmer.PacketType.AckCommandProcessed:
                            if (sensorsChangePending)
                            {
                                pProfile.updateChannelsFromSensors();
                               // ShowChannelTextBoxes();

                                //if (pSaveToFile)
                                //    WriteHeaderToFile();

                                sensorsChangePending = false;
                            }
                            break;

                        default:
                            break;
                    }



                }

                catch (System.TimeoutException)
                {
                    // do nothing

                    if (isStreaming == false && streamingDataRecieved == true)
                    {
                        // streaming is stopped, and some data has been recieved

                        CsvNullStream();
                        streamingDataRecieved = false;
                    }
                }
                catch (System.InvalidOperationException)
                {
                    // do nothing
                    // gets here if serial port is forcibly closed
                }
                catch (System.IO.IOException)
                {
                    // do nothing
                }
                /*
                catch
                {
                    // should really try to kill the thread more gracefully
                }
                */
            }
            // only stop reading when disconnecting, so disconnect serial port here too
            SerialClosePort();

            //if (stopReading && pCsvFile != null)
            //{
            //    CsvStopStream();
            //}
        }

        private TimeSpan TimeStampConvert(int timeTick)
        {
            // this method provides up to millisecond accuracy
            //double timeElapsedInSeconds = (double)timeTick / timeTickOffset;
            //int timeSeconds = (int)Math.Floor(timeElapsedInSeconds);
            //int timeMilli = (int)Math.Floor((timeElapsedInSeconds - timeSeconds) * 1000); // additional digits are truncated
            //return new TimeSpan(0, 0, 0, timeSeconds, timeMilli);

            // this method provides up to 100*nanosecond accuracy
            double timeElapsedInSeconds = (double)timeTick / timeTickOffset;
            long timeOHNano = (long)Math.Floor(timeElapsedInSeconds * oneHundredNanoSec);
            return new TimeSpan(timeOHNano);
        }

        private string TimeStampShimmerProcess(int shimmerTimeTick)
        {
            // notation clarification: the timeTick used in this function is the 32 kHz clock from the 
            // shimmer devices (ie timeTick/2^15 to get time in seconds). TimeStampConvert uses 
            // TimeSpan's tick field (ie 1 tick = 100 ns), to get things into an agreeable format with DateTime
            // To reduce the issues with rounding error, most of the values in this function is kept in shimmer timetick
            // format (= integer) and only converted to seconds (= TimeSpan.Tick) when interacting with DateTime

            int actualTimeStamp = 0;
            entriesRead += 1;

            if (streamTimeTick == -1)
            {
                // if this is the first timestamp to come in, note the "initial" time
                //streamTimeTick = streamRollOverInc - shimmerTimeTick;
                //streamTimeStamp = DateTime.Now.Subtract(TimeStampConvert(shimmerTimeTick));

                streamTimeTick = shimmerTimeTick;
                streamTimeStamp = DateTime.Now; //.Subtract(TimeStampConvert(shimmerTimeTick));
            }
            else if (previousTimeTick > shimmerTimeTick)
            {
                streamRollOver += 1;
            }
           
            if (streamRollOver == 0)
            { 
                // no rollover yet, don't include streamTimeTick
                actualTimeStamp = shimmerTimeTick - streamTimeTick;
            }
            else if (streamRollOver == 1)
            {
                // there is only one rollover, add only streamTimeTick
                actualTimeStamp = (streamRollOverInc - streamTimeTick) + shimmerTimeTick;
            }
            else 
            { 
                // multiple rollovers exist
                actualTimeStamp = (streamRollOverInc - streamTimeTick) + (streamRollOver - 1) * streamRollOverInc + shimmerTimeTick;
            }

            previousActualTimeTick = actualTimeStamp;
            previousTimeTick = shimmerTimeTick; // record the current timestamp as the previous

            //return streamTimeStamp.Add(TimeStampConvert(actualTimeStamp)).ToString("HH:mm:ss.fffffff");
            return streamTimeStamp.Add(TimeStampConvert(actualTimeStamp)).ToString("HH:mm:ss.fffffff");
            //return shimmerTimeTick.ToString();
        }

        public string TimeStampPollTimeSince()
        {
            // time elapsed (in ms) since start of streaming

            //return DateTime.Now.Subtract(cmdStartTimeStamp).TotalMilliseconds.ToString();
            //return DateTime.Now.Subtract(cmdStartTimeStamp).TotalSeconds.ToString();
            return DateTime.Now.Subtract(cmdStartTimeStamp).ToString("HH:mm:ss.fffffff");
        }

        private void SetTextChannels(ShimmerDataPacket packet)
        {
            try
            {
                if (pSaveToFile)
                {
                    pCsvFile.Write(TimeStampShimmerProcess(packet.GetTimeStamp())); // records the time attached by SHIMMER

                    for (int i = 0; i < packet.GetNumChannels(); i++)
                    {
                        pCsvFile.Write("," + packet.GetChannel(i).ToString());
                    }

                    pCsvFile.Write("," + DateTime.Now.ToLocalTime().ToString("HH:mm:ss.fffffff"));
                    pCsvFile.Write("," + packet.GetTimeStamp().ToString());
                    // pCsvFile.Write(TimeStampPollTimeSince()); // records the time clocked by the host

                    pCsvFile.Write("\n");
                }
               
            }
            catch (System.ArgumentOutOfRangeException)
            {
                CommandStopStreaming();
                serialPortShimmer.Close();
                string errStr = "Receiving incorrect or corrupt packets. Closing the connection...";
                throw (new System.ArgumentOutOfRangeException(errStr));

            }
        }

        public void ChangeConfiguration()
        {
            bool wait = false;          // give shimmer chance to process command before sending another

            if (serialPortShimmer.IsOpen)
            {
                if (pProfile.changeSamplingRate)
                {
                    serialPortShimmer.Write(new byte[2] { (byte)Shimmer.PacketType.SetSamplingRateCommand, 
                        (byte)pProfile.GetAdcSamplingRate() }, 0, 2);
                    pProfile.changeSamplingRate = false;
                    wait = true;
                }

                if (pProfile.changeSensors)
                {
                    if (wait)
                    {
                        System.Threading.Thread.Sleep(500);
                    }
                    serialPortShimmer.Write(new byte[2] { (byte)Shimmer.PacketType.SetSensorsCommand,
                        (byte)pProfile.GetSensors()}, 0, 2);
                    pProfile.changeSensors = false;
                    wait = true;
                    sensorsChangePending = true;
                }

                if (pProfile.change5Vreg)
                {
                    if (wait)
                        System.Threading.Thread.Sleep(500);
                    serialPortShimmer.Write(new byte[2] { (byte)Shimmer.PacketType.Set5VRegulatorCommand,
                        (pProfile.GetVReg()?(byte)1:(byte)0)}, 0, 2);
                    pProfile.change5Vreg = false;
                    wait = true;
                }

                if (pProfile.changePwrMux)
                {
                    if (wait)
                        System.Threading.Thread.Sleep(500);
                    serialPortShimmer.Write(new byte[2] { (byte)Shimmer.PacketType.SetPowerMuxCommand,
                        (pProfile.GetPMux()?(byte)1:(byte)0)}, 0, 2);
                    pProfile.changePwrMux = false;
                    wait = true;
                    sensorsChangePending = true;
                }

                if (pProfile.changeAccelSens)
                {
                    if (wait)
                        System.Threading.Thread.Sleep(500);
                    serialPortShimmer.Write(new byte[2] { (byte)Shimmer.PacketType.SetAccelSensitivityCommand,
                        (byte)pProfile.GetAccelSensitivity()}, 0, 2);
                    pProfile.changeAccelSens = false;
                }

                // give the shimmer time to make the changes before continuing (required?)
                System.Threading.Thread.Sleep(500);
            }            
        }

        public void WriteHeaderToFile()
        {
            pCsvFile.Write("TimeStamp");
            for (int i = 0; i < pProfile.GetNumChannels(); i++)
            {
                if ((pProfile.GetChannel(i) == (int)Shimmer.ChannelContents.AnExA0) && pProfile.GetPMux())
                {
                    pCsvFile.Write("," + "VSenseReg");
                }
                else if ((pProfile.GetChannel(i) == (int)Shimmer.ChannelContents.AnExA7) && pProfile.GetPMux())
                {
                    pCsvFile.Write("," + "VSenseBatt");
                }
                else
                {
                    pCsvFile.Write("," + Enum.GetName(typeof(Shimmer.ChannelContents), pProfile.GetChannel(i)));
                }
            }
            pCsvFile.Write("\n");
        }

        public void CloseVariable()
        {
            // destructor function
            // (TODO rename to ~ShimmerDevice so don't need to explicitly call?)

            if (isStreaming)
            {
                // if the SHIMMER is still streaming, stop the streaming
                CommandStopStreaming();
            }

            if (!stopReading)
            {
                // if thread is still reading, stop it
                stopReading = true;
            }

            if (readThread != null)
            {
                // and kill the ReadData thread, if not down already
                ReadDataEnd();
            }

            if (pCsvFile != null)
            {
                // terminate all the filewriting
                CsvStopStream();
                CsvNullStream();
            }

            if (SerialIsOpen())
            {
                // finally, if serial port is still open, close it
                SerialClosePort();
            }
        }

        ~ShimmerDevice()
        {
            CloseVariable();
        }
    }

    // ------------------------------------------------------------------------------------------------------------------------------------------------------------
    // holds a collection of SHIMMER devices
    class ShimmerDeviceCollection
    {
        // internal specific variables
        private int shimmerDeviceArrayLength;
        private ShimmerDevice[] shimmerDeviceArray;

        private string filePathStrPrefix;

        private bool connectionStatus = false;


        // external specific variables
        private System.ComponentModel.IContainer components; // TODO not sure if this is important at all...
        private ShimmerProfile pProfile;
        private ShimmerApplicationSettings pSettings;

        // constructor
        public ShimmerDeviceCollection(System.ComponentModel.IContainer Components, ShimmerProfile PProfile, ShimmerApplicationSettings PSettings)
        {
            components = Components;
            pProfile = PProfile;
            pSettings = PSettings;

            shimmerDeviceArrayLength = pSettings.SerialPortConnection.GetLength(0);
            shimmerDeviceArray = new ShimmerDevice[shimmerDeviceArrayLength];

            //filePathStrPrefix = FilePathStrPrefixUpdate();

            InitalizeCollection();
        }

        public bool GetConnectionStatus()
        {
            return connectionStatus;
        }

        public string FilePathStrPrefixUpdate(DateTime currentTimeStamp)
        {
            return pSettings.CSVFilePath + currentTimeStamp.ToString("yy-MM-dd-HH-mm-ss");
        }

        private void InitalizeCollection()
        {
            // initalize devicearray and specify the specific indices
            string port, btName;

            for (int i = 0; i < pSettings.SerialPortConnection.Length; i++)
            {
                port = pSettings.SerialPortConnection[i];
                btName = pSettings.SerialPortConnectBtName[i];

                shimmerDeviceArray[i] = new ShimmerDevice(components, pProfile, pSettings, port, btName);
            }
        }

        public void OpenAndConnect()
        {
            /// <summary>
            /// replicates the functionality of 'Control.btnConnect_Click'. Opens the serial connections
            /// </summary>

            for (int i = 0; i < shimmerDeviceArrayLength; i++)
            {
                try
                {
                    // open the serial connections of all the SHIMMERs
                    shimmerDeviceArray[i].SerialOpen();

                    // start the readdata threads for all the SHIMMERs
                    shimmerDeviceArray[i].ReadDataStart();

                    // Read SHIMMER Profile (makes sure that the profile update was commited properly)
                    shimmerDeviceArray[i].ProfileCheck();
                }
                catch (Exception)
                {
                    string ErrStr = "Cannot open " + shimmerDeviceArray[i].ToString();
                    throw (new SystemException(ErrStr));
                }

            }

            connectionStatus = true;
        }

        public void OpenAndConnect(ShimmerProfile ppProfile)
        {
            /// <summary>
            /// replicates the functionality of 'Control.btnConnect_Click'. Opens the serial connections
            /// </summary>

            pProfile = ppProfile;
            OpenAndConnect();
        }

        public void ProfileUpdate(ShimmerProfile ppProfile)
        {
            pProfile = ppProfile;

            for (int i = 0; i < shimmerDeviceArrayLength; i++)
            {
                shimmerDeviceArray[i].ProfileUpdate(pProfile);
            }
        }

        public void Disconnect()
        {
            /// <summary>
            /// replicates the functionality of 'Control.Disconnect', disconnects all the attached SHIMMER systems
            /// </summary>

            if (connectionStatus)
            {
                for (int i = 0; i < shimmerDeviceArrayLength; i++)
                {
                    // issue stopRead for all devices
                    shimmerDeviceArray[i].SerialClose();
                }
            }
        }

        public void DataStreamStart()
        {
            /// <summary>
            /// replicates the functionality of 'Control.btnStart_Click', starts the data streaming
            /// </summary>

            DateTime currentTimeStamp = DateTime.Now;
            filePathStrPrefix = FilePathStrPrefixUpdate(currentTimeStamp);

            if (connectionStatus)
            {
                for (int i = 0; i < shimmerDeviceArrayLength; i++)
                {
                    // Start the CSV writing process
                    shimmerDeviceArray[i].CsvStartStream(filePathStrPrefix, currentTimeStamp);
                }

                 for (int i = 0; i < shimmerDeviceArrayLength; i++)
                {
                    // Start the CSV writing process
                    shimmerDeviceArray[i].CsvStartStreamCommand();
                }
            }
        }

        public void DataStreamStop()
        {
            /// <summary>
            /// replicates the functionality of 'Control.btnStop_Click', starts the data streaming
            /// </summary>

            if (connectionStatus)
            {
                for (int i = 0; i < shimmerDeviceArrayLength; i++)
                {
                    shimmerDeviceArray[i].CsvStopStream();
                }
            }
        }

        public bool ProfileIsFilled()
        {
            bool isFilled = true;
            bool tempFilled;

            for (int i = 0; i < shimmerDeviceArrayLength; i++)
            {
                tempFilled = shimmerDeviceArray[i].ProfileIsFilled();

                isFilled &= tempFilled;
            }

            return isFilled;
        }

        public override string ToString()
        {
            string outStr = "";

            for (int i = 0; i < shimmerDeviceArrayLength; i++)
            {
                outStr += shimmerDeviceArray[i].ToStringPortName() + ",";
            }

            return outStr.TrimEnd(',');
        }

        public void CloseVariable()
        {
            // destructor function 
            // (TODO rename to ~ShimmerDeviceCollection so don't need to explicitly call?)

            // close all the ports and flush all streams of the collection
            for (int i = 0; i < shimmerDeviceArrayLength; i++)
            {
                shimmerDeviceArray[i].CloseVariable();
            }

            connectionStatus = false;
        }

        ~ShimmerDeviceCollection()
        {
            // destructor function

            CloseVariable();
        }

    }
}
